package com.ihr360.payroll.service.salarytasklet.step.processor;

import com.ihr360.alps.api.ServiceProvider;
import com.ihr360.attendance.AttendancePeriodReport;
import com.ihr360.common.DateRange;
import com.ihr360.commons.exception.Ihr360Exception;
import com.ihr360.commons.lang.CollectionUtils;
import com.ihr360.commons.lang.DateUtils;
import com.ihr360.commons.model.meta.table.DynamicCell;
import com.ihr360.commons.model.type.EnrollAdjustFormula;
import com.ihr360.commons.model.type.SalaryFieldValueType;
import com.ihr360.expr.ExprArray;
import com.ihr360.expr.ExprString;
import com.ihr360.expr.core.Expr;
import com.ihr360.expr.engine.DependencyEngine;
import com.ihr360.expr.engine.EngineProvider;
import com.ihr360.expr.engine.VariableID;
import com.ihr360.job.core.annotation.AfterChunk;
import com.ihr360.job.core.annotation.BeforeProcess;
import com.ihr360.job.core.item.ItemProcessor;
import com.ihr360.job.core.logger.CalcLog;
import com.ihr360.job.core.repository.JobRepository;
import com.ihr360.job.core.scope.context.SimpleStepContext;
import com.ihr360.payperiod.PayPeriodTerm;
import com.ihr360.payroll.calc.StaffSalaryCalcPeriod;
import com.ihr360.payroll.calc.StaffSalaryCalcPeriodTerm;
import com.ihr360.payroll.context.SimpleStaffPayrollInfo;
import com.ihr360.payroll.dao.SalaryCalcMiddleDataRepository;
import com.ihr360.payroll.dao.StaffPayrollInfoRepository;
import com.ihr360.payroll.model.dto.salary.basecode.SalaryFieldType;
import com.ihr360.payroll.model.dto.salary.embeddable.SalaryFieldValue;
import com.ihr360.payroll.model.dto.salary.embeddable.StaffInfoSnapshot;
import com.ihr360.payroll.model.dto.salary.meta.SalaryField;
import com.ihr360.payroll.model.dto.salary.task.period.SalaryCalcMiddleData;
import com.ihr360.payroll.model.dto.salary.task.period.StaffPayPeriodTermCalcContext;
import com.ihr360.payroll.model.dto.salary.task.period.embeddable.CalcMiddleRow;
import com.ihr360.payroll.model.dto.salary.task.period.embeddable.StaffSalaryCalcContext;
import com.ihr360.payroll.model.dto.salary.task.period.embeddable.StaffSalarySnapshot;
import com.ihr360.payroll.model.dto.salary.task.period.embeddable.StaffSalaryTermId;
import com.ihr360.payroll.model.dto.staff.StaffPayrollInfo;
import com.ihr360.payroll.service.attendancereport.AttendanceBillService;
import com.ihr360.payroll.service.attendancereport.AttendanceMonthlyReportService;
import com.ihr360.payroll.service.payperiod.StaffPayPeriodTermSalaryService;
import com.ihr360.payroll.service.salarytasklet.SalaryReferenceDataService;
import com.ihr360.payroll.service.salarytasklet.config.TaskletConfig;
import com.ihr360.payroll.service.salarytasklet.expr.engine.SalaryDependencyEngine;
import com.ihr360.payroll.service.salarytasklet.step.SalaryCalcContext;
import com.ihr360.payroll.service.salarytasklet.step.calculator.PersonalCalcLog;
import com.ihr360.payroll.service.salarytasklet.step.calculator.PersonalSalaryCalculator;
import com.ihr360.validation.ValidationUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author davidwei
 *         薪资计算处理器
 */
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class SalaryCalcProcessor implements ItemProcessor<List<StaffPayPeriodTermCalcContext>, Map<String, StaffSalaryCalcPeriod>> {
    final Logger logger = LoggerFactory.getLogger(DependencyEngine.class.getName());
    private SimpleStepContext simpleStepContext;


    @Autowired
    private TaskletConfig taskletConfig;

    @Autowired
    private SalaryReferenceDataService salaryReferenceDataService;

    @Autowired
    private StaffPayPeriodTermSalaryService staffPayPeriodTermSalaryService;

    @Autowired
    private AttendanceMonthlyReportService attendanceMonthlyReportService;

    @Autowired
    private AttendanceBillService attendanceBillService;

    @Autowired
    private StaffPayrollInfoRepository staffPayrollInfoRepository;

    @Autowired
    private SalaryCalcMiddleDataRepository salaryCalcMiddleDataRepository;

    @Autowired
    private JobRepository jobRepository;


    private SalaryCalcContext salaryCalcContext;

    private DateRange payrollPeriodRange;

    private DateRange attendancePeriodRange;

    private boolean isSamePeriod = false;

    Map<String, Map> salaryTaxTableMap;

    private Set<String> staffRosterFields;

    Map<String, SalaryField> rosterSalaryFieldMap;

    private Map<StaffInfoSnapshot, PersonalCalcLog> logMap = new HashMap<>();

    private PersonalSalaryCalculator calculator = new PersonalSalaryCalculator();


    public void setSalaryCalcContext(SalaryCalcContext salaryCalcContext) {
        this.salaryCalcContext = salaryCalcContext;
        ValidationUtils.rejectIfEmpty(salaryCalcContext, "[薪资计算]", "计算上下文变量为空");
        this.calculator = new PersonalSalaryCalculator();
        salaryTaxTableMap = salaryCalcContext.taxTableHolder.getWrittenItems();
        setPeriodRanges(salaryCalcContext.payrollPeriodRange, salaryCalcContext.attendancePeriodRange);
    }

    private void setPeriodRanges(DateRange payrollPeriodRange, DateRange attendancePeriodRange) {
        this.payrollPeriodRange = payrollPeriodRange;
        this.attendancePeriodRange = attendancePeriodRange;
        if (payrollPeriodRange.equals(attendancePeriodRange)) {
            isSamePeriod = true;
        } else {
            isSamePeriod = false;
        }
    }

    /**
     * 考勤周期和薪资周期不一致
     * case1
     * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     * +       (转正)
     * +
     * +---------+------1---------------------------------------------31
     * (加班)             (加班)
     * +26------28------1------------------------------------25
     * +
     * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     * <p>
     * * case2
     * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     * +                             (转正)
     * +
     * +----------------1--------------+------------------------------31
     * (加班)             (加班)           (加班)
     * +26--------------1------------------------------------25
     * +
     * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     * <p>
     * 考勤周期和薪资周期一致
     * case1
     * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     * +                   (转正)
     * +
     * +1------------------—+--------------------------31+
     * (加班)                (加班)
     * +
     * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     *
     * @param calcContexts
     * @return
     * @throws Exception
     */
    @Override
    public Map<String, StaffSalaryCalcPeriod> process(List<StaffPayPeriodTermCalcContext> calcContexts) throws Exception {
        try {
            //获取员工纳税地区，纳税类型和工种
            String companyId = salaryCalcContext.getCompanyId();
            SalaryDependencyEngine engine = salaryCalcContext.dependencyEngineHolder.getWrittenItem();
            Map<String, Set<String>> references = engine.getSalaryReferences();
            if (CollectionUtils.isNotEmpty(references)) {
                staffRosterFields = references.get(ServiceProvider.SERVICE_ID.STAFF_ROSTER);
            }

            Map<String, SalaryField> salaryFieldMap = salaryCalcContext.salaryFieldsHolder.getWrittenItem();
            if (rosterSalaryFieldMap == null) {
                rosterSalaryFieldMap = new HashMap<>(20);
            } else {
                rosterSalaryFieldMap.clear();
            }
            for (Map.Entry<String, SalaryField> entry : salaryFieldMap.entrySet()) {
                SalaryField value = entry.getValue();
                SalaryFieldType type = value.getType();
                if (type != null) {
                    //将考勤部分数据加入分段你数据中
                    if (SalaryFieldType.Code.staff.equals(type.getCode())) {
                        rosterSalaryFieldMap.put(entry.getKey(), entry.getValue());
                    }
                }
            }

            ValidationUtils.rejectIfEmpty(companyId, null, "无法获取公司标识");

            //如果没有perid或者period里面没有term或者只要一个term返回true,否则表示有多个term，返回false
            Map<Boolean, List<StaffPayPeriodTermCalcContext>> singleOrMultiplePeriodMap = calcContexts.stream().collect(Collectors.groupingBy(s ->
                    {
                        if (salaryCalcContext.isPhasedCalcDisabled()) {
                            return true;
                        }
                        return s.isSinglePhase();
                    }
            ));

            this.calculator.setPayRollPeriod(this.payrollPeriodRange);
            this.calculator.setAttendancePeriod(this.attendancePeriodRange);
            this.calculator.initContext(salaryCalcContext);


            List<StaffPayPeriodTermCalcContext> staffCalcContexts = singleOrMultiplePeriodMap.get(Boolean.TRUE);
            Map<String, StaffSalaryCalcPeriod> result = new HashMap<>(singleOrMultiplePeriodMap.size());
            if (CollectionUtils.isNotEmpty(staffCalcContexts)) {
                this.calculator.setIsSinglePhase(true);
                this.calculator.setIsSingleTerm(true);

                if (CollectionUtils.isNotEmpty(staffCalcContexts)) {
                    processStaffs(staffCalcContexts, context -> {
                        StaffSalaryCalcPeriod res = calculator.processSinglePhased();
                        res.setCompanyId(companyId);
                        res.setDepartmentName(context.getDepartmentName());
                        res.setMobileNo(context.getMobileNo());
                        res.setDepartmentId(context.getDepartmentId());
                        res.setSihfStatus(context.getSihfStatus());
                        res.setAttendanceStatus(context.getAttendanceStatus());
                        result.put(res.getStaffId(), res);
                        return res;
                    });
                }
            }

            staffCalcContexts = singleOrMultiplePeriodMap.get(Boolean.FALSE);
            if (CollectionUtils.isNotEmpty(staffCalcContexts)) {
                this.calculator.setIsSinglePhase(false);
                this.calculator.setIsSingleTerm(true);
                processStaffs(staffCalcContexts, context -> {
                    StaffSalaryCalcPeriod res = calculator.processMultiplePhased();
                    res.setCompanyId(companyId);
                    res.setDepartmentName(context.getDepartmentName());
                    res.setMobileNo(context.getMobileNo());
                    res.setDepartmentId(context.getDepartmentId());
                    res.setSihfStatus(context.getSihfStatus());
                    res.setAttendanceStatus(context.getAttendanceStatus());
                    result.put(res.getStaffId(), res);
                    return res;
                });
            }
            if (CollectionUtils.isNotEmpty(logMap)) {
                //save logger oneshot
                List<CalcLog> allLogs = new ArrayList<>();
                for (Map.Entry<StaffInfoSnapshot, PersonalCalcLog> entry : logMap.entrySet()) {
                    PersonalCalcLog personalCalcLog = entry.getValue();
                    if (CollectionUtils.isNotEmpty(personalCalcLog.getLogs())) {
                        allLogs.addAll(personalCalcLog.getLogs());
                    }
                }
                if (CollectionUtils.isNotEmpty(allLogs)) {
                    jobRepository.logInBatch(simpleStepContext, allLogs);
                }
                logMap.clear();
            }

            return result;
        } catch (Ihr360Exception e) {
            e.printStackTrace();
            throw e;
        }
    }

    private void processStaffs(
            List<StaffPayPeriodTermCalcContext> staffs, ProcessSalaryCalc callback) {
        StopWatch clock = new StopWatch();
        for (StaffPayPeriodTermCalcContext staffCalcContext : staffs) {
            clock.start("==========StopWatch:" + staffCalcContext.getId().getStaffId());
            String staffId = staffCalcContext.getId().getStaffId();
            StaffSalaryCalcContext calcContext = staffCalcContext.getContext();

            SimpleStaffPayrollInfo payrollInfo = calcContext.getStaffInfo();
            //输入项目部分
            Map<String, DynamicCell> inputData = calcContext.getInputData();
            //员工的服务引用数据
            Map<String, Map<String, Object>> staffRefServiceData = calcContext.getRefData();
            if (staffRefServiceData != null) {
                Map<String, Object> rosterInfo = staffRefServiceData.get(ServiceProvider.SERVICE_ID.STAFF_ROSTER);
                if (CollectionUtils.isNotEmpty(staffRosterFields)) {
                    if (rosterInfo == null) {
                        rosterInfo = new HashMap<>();
                    }
                    for (String field : staffRosterFields) {
                        if (!rosterInfo.containsKey(field) || rosterInfo.get(field) == null) {
                            //HOT FIX system salary field name must be same as roster field name
                            SalaryField salaryField = rosterSalaryFieldMap.get(field.toLowerCase());
                            if (salaryField != null) {
                                SalaryFieldValue valueMeta = salaryField.getValueMeta();
                                if (valueMeta != null) {
                                    SalaryFieldValueType valueType = valueMeta.getValueType();
                                    if (SalaryFieldValueType.TEXT.equals(valueType) ||
                                            SalaryFieldValueType.DATE.equals(valueType) ||
                                            SalaryFieldValueType.DATETIME.equals(valueType)) {
                                        rosterInfo.put(field, "");
                                    }
                                }
                            }
                        }
                    }
                    staffRefServiceData.put(ServiceProvider.SERVICE_ID.STAFF_ROSTER, rosterInfo);
                    if (EnrollAdjustFormula.FORMULA3.equals(salaryCalcContext.enrollAdjustFormula)) {
                        Object enrollDate = rosterInfo.get(ServiceProvider.STAFF_ROSTER.ENROLL_IN_DATE);
                        Object leaveDate = rosterInfo.get(ServiceProvider.STAFF_ROSTER.LEAVE_DATE);
                        int year = salaryCalcContext.getPayPeriodYear();
                        int month = salaryCalcContext.getPayPeriodMonth();
                        Pair<Integer, Integer> yearAndMonth = DateUtils.getYearAndMonth(enrollDate);
                        if (yearAndMonth.getFirst() == year || yearAndMonth.getSecond() == month) {
                            this.calculator.setEnrollIn(true);
                        }
                        Pair<Integer, Integer> yearAndMonth1 = DateUtils.getYearAndMonth(leaveDate);
                        if (yearAndMonth1.getFirst() == year || yearAndMonth1.getSecond() == month) {
                            this.calculator.setQuitOut(true);
                        }
                    }
                }
            }

            //设置考勤数据
            AttendancePeriodReport monthlyReport = calcContext.getAtm();
            List<AttendancePeriodReport> periodReports = calcContext.getAtps();

            DeepCopySalaryProfile deepCopySalaryProfile = new DeepCopySalaryProfile(calcContext).invoke();
            StaffSalaryCalcPeriod targetSalaryProfile = deepCopySalaryProfile.getTargetSalaryProfile();
            Set<String> profileFields = deepCopySalaryProfile.getProfileFields();


            //根据纳税类型获取税表
            Map<String, ExprArray> taxTables = salaryTaxTableMap.get(TaxTableProcessor.taxTableCode(payrollInfo.getTaxDivision(), payrollInfo.getTaxRole()));
            targetSalaryProfile.setStaffId(staffId);
            targetSalaryProfile.setStaffName(staffCalcContext.getStaffName());


            StaffSalarySnapshot basedOnTermSalary = calcContext.getBasedOnTermSalary();
            if (CollectionUtils.isEmpty(taxTables)) {
                jobRepository.error(simpleStepContext, "[薪资核算]", String.format("无法获取纳税地区%s的针对%s的税表",payrollInfo.getTaxDivisionName(),payrollInfo.getTaxRoleName()));
                continue;
            }

            this.calculator.setStaffId(staffId);
            this.calculator.setStaffName(staffCalcContext.getStaffName());
            this.calculator.setProfileFields(profileFields);
            this.calculator.setTaxTables(taxTables);
            this.calculator.setInputData(inputData);
            this.calculator.setReferenceData(staffRefServiceData);
            this.calculator.setPeriodSalaryProfile(targetSalaryProfile);
            this.calculator.setAttendanceMonthlyReport(monthlyReport);
            this.calculator.setAttendancePeriodReports(periodReports);
            this.calculator.setBasedOnTermSalary(basedOnTermSalary);
            if (this.calculator.getPersonalCalcultorEvent() == null) {
                this.calculator.setPersonalCalcultorEvent((sid, sn, p, e) -> {
                    if (taskletConfig.isRecordMidData()) {
                        PayPeriodTerm payPeriodTerm = salaryCalcContext.payPeriodTerm;
                        EngineProvider provider = e.getProvider();
                        Map<VariableID, Expr> values = provider.getValues();
                        int size = values != null ? values.size() : 0;
                        final Map<String, String> newValues = new HashMap<>(size);
                        if (values != null) {
                            values.entrySet().forEach(
                                    s -> newValues.put(s.getKey().getVariableId(), s.getValue().toString())
                            );
                        }

                        Map<String, Expr> phasedVariables = provider.getPhasedVariables();


                        Map<String, Expr> variables = provider.getVariables();
                        Map<String, Expr> personalVariables = p.getAllVariables();
                        CalcMiddleRow data = new CalcMiddleRow(newValues, exprMapToStrMap(variables),
                                exprMapToStrMap(phasedVariables), exprMapToStrMap(personalVariables));

                        SalaryCalcMiddleData row = new SalaryCalcMiddleData();
                        row.setId(new StaffSalaryTermId(payPeriodTerm.getId(), sid));
                        row.setCompanyId(salaryCalcContext.getCompanyId());
                        row.setStaffName(sn);
                        row.setData(data);
                        salaryCalcMiddleDataRepository.save(row);
                    }
                });
            }
            callback.process(staffCalcContext);

            PersonalCalcLog log = this.calculator.personalCalcLog();
            if (log != null && log.hasLog()) {
                StaffInfoSnapshot staffInfo = getLogKey(staffCalcContext);
                addLogToLogMap(staffInfo, log);
            }
            clock.stop();
            long timecost = clock.getLastTaskTimeMillis();
            if (logger.isInfoEnabled()) {
                logger.info("--time cost={}", timecost);
                logger.info(clock.getLastTaskInfo().toString());
            }

        }
    }

    private Map<String, String> exprMapToStrMap(Map<String, Expr> oldMap) {
        int size;
        size = oldMap != null ? oldMap.size() : 0;
        final Map<String, String> newMap = new HashMap<>(size);
        if (oldMap != null) {
            oldMap.entrySet().forEach(s -> {
                Expr expr = s.getValue();
                if (expr != null) {
                    if (expr instanceof ExprString) {
                        ExprString exprString = (ExprString) expr;
                        newMap.put(s.getKey(), exprString.str);
                    } else {
                        newMap.put(s.getKey(), s.getValue().toString());
                    }

                } else {
                    logger.info("key:{} has null value", s.getKey());
                }
            });
        }
        return newMap;
    }


    private void addLogToLogMap(StaffInfoSnapshot staffInfo, PersonalCalcLog log) {
        PersonalCalcLog calcLog = logMap.get(staffInfo);
        if (calcLog == null) {
            logMap.put(staffInfo, log);
        } else {
            calcLog.addLogs(calcLog.getLogs());
        }
    }

    private StaffInfoSnapshot getLogKey(StaffPayPeriodTermCalcContext context) {
        StaffInfoSnapshot staffInfo = new StaffInfoSnapshot();
        staffInfo.setStaffId(context.getId().getStaffId());
        staffInfo.setStaffName(context.getStaffName());
        return staffInfo;
    }

    interface ProcessSalaryCalc {
        /**
         * 薪资计算回调函数
         *
         * @param staffPayPeriodTermCalcContext
         * @return
         */
        StaffSalaryCalcPeriod process(StaffPayPeriodTermCalcContext staffPayPeriodTermCalcContext);
    }

    @BeforeProcess
    public void setSimpleStepContext(List<StaffPayPeriodTermCalcContext> calcContexts, SimpleStepContext simpleStepContext) {
        this.simpleStepContext = simpleStepContext;
    }

    @AfterChunk
    public void onAfterChunk() {
        if (taskletConfig.isRecordMidData()) {

        }
    }

    private class DeepCopySalaryProfile {
        private StaffSalaryCalcContext calcContext;
        private StaffSalaryCalcPeriod targetSalaryProfile;
        private Set<String> profileFields;

        public DeepCopySalaryProfile(StaffSalaryCalcContext calcContext) {
            this.calcContext = calcContext;
        }

        public StaffSalaryCalcPeriod getTargetSalaryProfile() {
            return targetSalaryProfile;
        }

        public Set<String> getProfileFields() {
            return profileFields;
        }

        public DeepCopySalaryProfile invoke() {
            StaffSalaryCalcPeriod salaryProfile = calcContext.getSpData();

            targetSalaryProfile = new StaffSalaryCalcPeriod();

            BeanUtils.copyProperties(salaryProfile, targetSalaryProfile, "phases");

            List<StaffSalaryCalcPeriodTerm> phases = new ArrayList<>();
            profileFields = new HashSet<>();
            if (CollectionUtils.isNotEmpty(salaryProfile.getPhases())) {
                salaryProfile.getPhases().forEach(
                        s -> {
                            StaffSalaryCalcPeriodTerm t = new StaffSalaryCalcPeriodTerm();
                            t.setEndOn(s.getEndOn());
                            t.setStartOn(s.getStartOn());
                            Map<String, DynamicCell> items = s.getItems();
                            if (CollectionUtils.isNotEmpty(items)) {
                                Map<String, DynamicCell> newItems = new HashMap<>(items.size());
                                for (Map.Entry<String, DynamicCell> entry : items.entrySet()) {
                                    String field = entry.getKey();
                                    if (!profileFields.contains(field)) {
                                        profileFields.add(field);
                                    }
                                    newItems.put(field, entry.getValue());
                                }
                                t.setItems(newItems);
                            }
                            phases.add(t);
                        }
                );
            }
            targetSalaryProfile.setPhases(phases);
            return this;
        }
    }
}